home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2005 October / PCWOCT05.iso / Software / FromTheMag / XAMPP 1.4.14 / xampp-win32-1.4.14-installer.exe / xampp / php / pear / Mail / RFC822.php < prev    next >
PHP Script  |  2004-10-01  |  31KB  |  907 lines

  1. <?php
  2. // +-----------------------------------------------------------------------+
  3. // | Copyright (c) 2001-2002, Richard Heyes                                |
  4. // | All rights reserved.                                                  |
  5. // |                                                                       |
  6. // | Redistribution and use in source and binary forms, with or without    |
  7. // | modification, are permitted provided that the following conditions    |
  8. // | are met:                                                              |
  9. // |                                                                       |
  10. // | o Redistributions of source code must retain the above copyright      |
  11. // |   notice, this list of conditions and the following disclaimer.       |
  12. // | o Redistributions in binary form must reproduce the above copyright   |
  13. // |   notice, this list of conditions and the following disclaimer in the |
  14. // |   documentation and/or other materials provided with the distribution.| 
  15. // | o The names of the authors may not be used to endorse or promote      |
  16. // |   products derived from this software without specific prior written  |
  17. // |   permission.                                                         |
  18. // |                                                                       |
  19. // | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   |
  20. // | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     |
  21. // | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
  22. // | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  |
  23. // | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
  24. // | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      |
  25. // | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
  26. // | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
  27. // | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   |
  28. // | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
  29. // | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
  30. // |                                                                       |
  31. // +-----------------------------------------------------------------------+
  32. // | Authors: Richard Heyes <richard@phpguru.org>                          |
  33. // |          Chuck Hagenbuch <chuck@horde.org>                            |
  34. // +-----------------------------------------------------------------------+
  35.  
  36. /**
  37. * RFC 822 Email address list validation Utility
  38. *
  39. * What is it?
  40. *
  41. * This class will take an address string, and parse it into it's consituent
  42. * parts, be that either addresses, groups, or combinations. Nested groups
  43. * are not supported. The structure it returns is pretty straight forward,
  44. * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use
  45. * print_r() to view the structure.
  46. *
  47. * How do I use it?
  48. *
  49. * $address_string = 'My Group: "Richard" <richard@localhost> (A comment), ted@example.com (Ted Bloggs), Barney;';
  50. * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true)
  51. * print_r($structure);
  52. *
  53. * @author  Richard Heyes <richard@phpguru.org>
  54. * @author  Chuck Hagenbuch <chuck@horde.org>
  55. * @version $Revision: 1.10 $
  56. * @license BSD
  57. * @package Mail
  58. */
  59.  
  60. class Mail_RFC822 {
  61.  
  62.     /**
  63.      * The address being parsed by the RFC822 object.
  64.      * @var string $address
  65.      */
  66.     var $address = '';
  67.  
  68.     /**
  69.      * The default domain to use for unqualified addresses.
  70.      * @var string $default_domain
  71.      */
  72.     var $default_domain = 'localhost';
  73.  
  74.     /**
  75.      * Should we return a nested array showing groups, or flatten everything?
  76.      * @var boolean $nestGroups
  77.      */
  78.     var $nestGroups = true;
  79.  
  80.     /**
  81.      * Whether or not to validate atoms for non-ascii characters.
  82.      * @var boolean $validate
  83.      */
  84.     var $validate = true;
  85.  
  86.     /**
  87.      * The array of raw addresses built up as we parse.
  88.      * @var array $addresses
  89.      */
  90.     var $addresses = array();
  91.  
  92.     /**
  93.      * The final array of parsed address information that we build up.
  94.      * @var array $structure
  95.      */
  96.     var $structure = array();
  97.  
  98.     /**
  99.      * The current error message, if any.
  100.      * @var string $error
  101.      */
  102.     var $error = null;
  103.  
  104.     /**
  105.      * An internal counter/pointer.
  106.      * @var integer $index
  107.      */
  108.     var $index = null;
  109.  
  110.     /**
  111.      * The number of groups that have been found in the address list.
  112.      * @var integer $num_groups
  113.      * @access public
  114.      */
  115.     var $num_groups = 0;
  116.  
  117.     /**
  118.      * A variable so that we can tell whether or not we're inside a
  119.      * Mail_RFC822 object.
  120.      * @var boolean $mailRFC822
  121.      */
  122.     var $mailRFC822 = true;
  123.     
  124.     /**
  125.     * A limit after which processing stops
  126.     * @var int $limit
  127.     */
  128.     var $limit = null;
  129.  
  130.  
  131.     /**
  132.      * Sets up the object. The address must either be set here or when
  133.      * calling parseAddressList(). One or the other.
  134.      *
  135.      * @access public
  136.      * @param string  $address         The address(es) to validate.
  137.      * @param string  $default_domain  Default domain/host etc. If not supplied, will be set to localhost.
  138.      * @param boolean $nest_groups     Whether to return the structure with groups nested for easier viewing.
  139.      * @param boolean $validate        Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
  140.      * 
  141.      * @return object Mail_RFC822 A new Mail_RFC822 object.
  142.      */
  143.     function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
  144.     {
  145.         if (isset($address))        $this->address        = $address;
  146.         if (isset($default_domain)) $this->default_domain = $default_domain;
  147.         if (isset($nest_groups))    $this->nestGroups     = $nest_groups;
  148.         if (isset($validate))       $this->validate       = $validate;
  149.         if (isset($limit))          $this->limit          = $limit;
  150.     }
  151.  
  152.  
  153.     /**
  154.      * Starts the whole process. The address must either be set here
  155.      * or when creating the object. One or the other.
  156.      *
  157.      * @access public
  158.      * @param string  $address         The address(es) to validate.
  159.      * @param string  $default_domain  Default domain/host etc.
  160.      * @param boolean $nest_groups     Whether to return the structure with groups nested for easier viewing.
  161.      * @param boolean $validate        Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
  162.      * 
  163.      * @return array A structured array of addresses.
  164.      */
  165.     function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
  166.     {
  167.  
  168.         if (!isset($this->mailRFC822)) {
  169.             $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit);
  170.             return $obj->parseAddressList();
  171.         }
  172.  
  173.         if (isset($address))        $this->address        = $address;
  174.         if (isset($default_domain)) $this->default_domain = $default_domain;
  175.         if (isset($nest_groups))    $this->nestGroups     = $nest_groups;
  176.         if (isset($validate))       $this->validate       = $validate;
  177.         if (isset($limit))          $this->limit          = $limit;
  178.  
  179.         $this->structure  = array();
  180.         $this->addresses  = array();
  181.         $this->error      = null;
  182.         $this->index      = null;
  183.  
  184.         while ($this->address = $this->_splitAddresses($this->address)) {
  185.             continue;
  186.         }
  187.         
  188.         if ($this->address === false || isset($this->error)) {
  189.             require_once 'PEAR.php';
  190.             return PEAR::raiseError($this->error);
  191.         }
  192.  
  193.         // Loop through all the addresses
  194.         for ($i = 0; $i < count($this->addresses); $i++){
  195.  
  196.             if (($return = $this->_validateAddress($this->addresses[$i])) === false
  197.                 || isset($this->error)) {
  198.                 require_once 'PEAR.php';
  199.                 return PEAR::raiseError($this->error);
  200.             }
  201.             
  202.             if (!$this->nestGroups) {
  203.                 $this->structure = array_merge($this->structure, $return);
  204.             } else {
  205.                 $this->structure[] = $return;
  206.             }
  207.         }
  208.  
  209.         return $this->structure;
  210.     }
  211.  
  212.     /**
  213.      * Splits an address into seperate addresses.
  214.      * 
  215.      * @access private
  216.      * @param string $address The addresses to split.
  217.      * @return boolean Success or failure.
  218.      */
  219.     function _splitAddresses($address)
  220.     {
  221.  
  222.         if (!empty($this->limit) AND count($this->addresses) == $this->limit) {
  223.             return '';
  224.         }
  225.  
  226.         if ($this->_isGroup($address) && !isset($this->error)) {
  227.             $split_char = ';';
  228.             $is_group   = true;
  229.         } elseif (!isset($this->error)) {
  230.             $split_char = ',';
  231.             $is_group   = false;
  232.         } elseif (isset($this->error)) {
  233.             return false;
  234.         }
  235.  
  236.         // Split the string based on the above ten or so lines.
  237.         $parts  = explode($split_char, $address);
  238.         $string = $this->_splitCheck($parts, $split_char);
  239.  
  240.         // If a group...
  241.         if ($is_group) {
  242.             // If $string does not contain a colon outside of
  243.             // brackets/quotes etc then something's fubar.
  244.  
  245.             // First check there's a colon at all:
  246.             if (strpos($string, ':') === false) {
  247.                 $this->error = 'Invalid address: ' . $string;
  248.                 return false;
  249.             }
  250.  
  251.             // Now check it's outside of brackets/quotes:
  252.             if (!$this->_splitCheck(explode(':', $string), ':'))
  253.                 return false;
  254.  
  255.             // We must have a group at this point, so increase the counter:
  256.             $this->num_groups++;
  257.         }
  258.  
  259.         // $string now contains the first full address/group.
  260.         // Add to the addresses array.
  261.         $this->addresses[] = array(
  262.                                    'address' => trim($string),
  263.                                    'group'   => $is_group
  264.                                    );
  265.  
  266.         // Remove the now stored address from the initial line, the +1
  267.         // is to account for the explode character.
  268.         $address = trim(substr($address, strlen($string) + 1));
  269.  
  270.         // If the next char is a comma and this was a group, then
  271.         // there are more addresses, otherwise, if there are any more
  272.         // chars, then there is another address.
  273.         if ($is_group && substr($address, 0, 1) == ','){
  274.             $address = trim(substr($address, 1));
  275.             return $address;
  276.  
  277.         } elseif (strlen($address) > 0) {
  278.             return $address;
  279.  
  280.         } else {
  281.             return '';
  282.         }
  283.  
  284.         // If you got here then something's off
  285.         return false;
  286.     }
  287.  
  288.     /**
  289.      * Checks for a group at the start of the string.
  290.      * 
  291.      * @access private
  292.      * @param string $address The address to check.
  293.      * @return boolean Whether or not there is a group at the start of the string.
  294.      */
  295.     function _isGroup($address)
  296.     {
  297.         // First comma not in quotes, angles or escaped:
  298.         $parts  = explode(',', $address);
  299.         $string = $this->_splitCheck($parts, ',');
  300.  
  301.         // Now we have the first address, we can reliably check for a
  302.         // group by searching for a colon that's not escaped or in
  303.         // quotes or angle brackets.
  304.         if (count($parts = explode(':', $string)) > 1) {
  305.             $string2 = $this->_splitCheck($parts, ':');
  306.             return ($string2 !== $string);
  307.         } else {
  308.             return false;
  309.         }
  310.     }
  311.  
  312.     /**
  313.      * A common function that will check an exploded string.
  314.      * 
  315.      * @access private
  316.      * @param array $parts The exloded string.
  317.      * @param string $char  The char that was exploded on.
  318.      * @return mixed False if the string contains unclosed quotes/brackets, or the string on success.
  319.      */
  320.     function _splitCheck($parts, $char)
  321.     {
  322.         $string = $parts[0];
  323.  
  324.         for ($i = 0; $i < count($parts); $i++) {
  325.             if ($this->_hasUnclosedQuotes($string)
  326.                 || $this->_hasUnclosedBrackets($string, '<>')
  327.                 || $this->_hasUnclosedBrackets($string, '[]')
  328.                 || $this->_hasUnclosedBrackets($string, '()')
  329.                 || substr($string, -1) == '\\') {
  330.                 if (isset($parts[$i + 1])) {
  331.                     $string = $string . $char . $parts[$i + 1];
  332.                 } else {
  333.                     $this->error = 'Invalid address spec. Unclosed bracket or quotes';
  334.                     return false;
  335.                 }
  336.             } else {
  337.                 $this->index = $i;
  338.                 break;
  339.             }
  340.         }
  341.  
  342.         return $string;
  343.     }
  344.  
  345.     /**
  346.      * Checks if a string has an unclosed quotes or not.
  347.      * 
  348.      * @access private
  349.      * @param string $string The string to check.
  350.      * @return boolean True if there are unclosed quotes inside the string, false otherwise.
  351.      */
  352.     function _hasUnclosedQuotes($string)
  353.     {
  354.         $string     = explode('"', $string);
  355.         $string_cnt = count($string);
  356.  
  357.         for ($i = 0; $i < (count($string) - 1); $i++)
  358.             if (substr($string[$i], -1) == '\\')
  359.                 $string_cnt--;
  360.  
  361.         return ($string_cnt % 2 === 0);
  362.     }
  363.  
  364.     /**
  365.      * Checks if a string has an unclosed brackets or not. IMPORTANT:
  366.      * This function handles both angle brackets and square brackets;
  367.      * 
  368.      * @access private
  369.      * @param string $string The string to check.
  370.      * @param string $chars  The characters to check for.
  371.      * @return boolean True if there are unclosed brackets inside the string, false otherwise.
  372.      */
  373.     function _hasUnclosedBrackets($string, $chars)
  374.     {
  375.         $num_angle_start = substr_count($string, $chars[0]);
  376.         $num_angle_end   = substr_count($string, $chars[1]);
  377.  
  378.         $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
  379.         $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
  380.  
  381.         if ($num_angle_start < $num_angle_end) {
  382.             $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
  383.             return false;
  384.         } else {
  385.             return ($num_angle_start > $num_angle_end);
  386.         }
  387.     }
  388.  
  389.     /**
  390.      * Sub function that is used only by hasUnclosedBrackets().
  391.      * 
  392.      * @access private
  393.      * @param string $string The string to check.
  394.      * @param integer &$num    The number of occurences.
  395.      * @param string $char   The character to count.
  396.      * @return integer The number of occurences of $char in $string, adjusted for backslashes.
  397.      */
  398.     function _hasUnclosedBracketsSub($string, &$num, $char)
  399.     {
  400.         $parts = explode($char, $string);
  401.         for ($i = 0; $i < count($parts); $i++){
  402.             if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i]))
  403.                 $num--;
  404.             if (isset($parts[$i + 1]))
  405.                 $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
  406.         }
  407.         
  408.         return $num;
  409.     }
  410.  
  411.     /**
  412.      * Function to begin checking the address.
  413.      *
  414.      * @access private
  415.      * @param string $address The address to validate.
  416.      * @return mixed False on failure, or a structured array of address information on success.
  417.      */
  418.     function _validateAddress($address)
  419.     {
  420.         $is_group = false;
  421.  
  422.         if ($address['group']) {
  423.             $is_group = true;
  424.  
  425.             // Get the group part of the name
  426.             $parts     = explode(':', $address['address']);
  427.             $groupname = $this->_splitCheck($parts, ':');
  428.             $structure = array();
  429.  
  430.             // And validate the group part of the name.
  431.             if (!$this->_validatePhrase($groupname)){
  432.                 $this->error = 'Group name did not validate.';
  433.                 return false;
  434.             } else {
  435.                 // Don't include groups if we are not nesting
  436.                 // them. This avoids returning invalid addresses.
  437.                 if ($this->nestGroups) {
  438.                     $structure = new stdClass;
  439.                     $structure->groupname = $groupname;
  440.                 }
  441.             }
  442.  
  443.             $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':')));
  444.         }
  445.  
  446.         // If a group then split on comma and put into an array.
  447.         // Otherwise, Just put the whole address in an array.
  448.         if ($is_group) {
  449.             while (strlen($address['address']) > 0) {
  450.                 $parts       = explode(',', $address['address']);
  451.                 $addresses[] = $this->_splitCheck($parts, ',');
  452.                 $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ',')));
  453.             }
  454.         } else {
  455.             $addresses[] = $address['address'];
  456.         }
  457.  
  458.         // Check that $addresses is set, if address like this:
  459.         // Groupname:;
  460.         // Then errors were appearing.
  461.         if (!isset($addresses)){
  462.             $this->error = 'Empty group.';
  463.             return false;
  464.         }
  465.  
  466.         for ($i = 0; $i < count($addresses); $i++) {
  467.             $addresses[$i] = trim($addresses[$i]);
  468.         }
  469.  
  470.         // Validate each mailbox.
  471.         // Format could be one of: name <geezer@domain.com>
  472.         //                         geezer@domain.com
  473.         //                         geezer
  474.         // ... or any other format valid by RFC 822.
  475.         array_walk($addresses, array($this, 'validateMailbox'));
  476.  
  477.         // Nested format
  478.         if ($this->nestGroups) {
  479.             if ($is_group) {
  480.                 $structure->addresses = $addresses;
  481.             } else {
  482.                 $structure = $addresses[0];
  483.             }
  484.  
  485.         // Flat format
  486.         } else {
  487.             if ($is_group) {
  488.                 $structure = array_merge($structure, $addresses);
  489.             } else {
  490.                 $structure = $addresses;
  491.             }
  492.         }
  493.  
  494.         return $structure;
  495.     }
  496.  
  497.     /**
  498.      * Function to validate a phrase.
  499.      *
  500.      * @access private
  501.      * @param string $phrase The phrase to check.
  502.      * @return boolean Success or failure.
  503.      */
  504.     function _validatePhrase($phrase)
  505.     {
  506.         // Splits on one or more Tab or space.
  507.         $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
  508.  
  509.         $phrase_parts = array();
  510.         while (count($parts) > 0){
  511.             $phrase_parts[] = $this->_splitCheck($parts, ' ');
  512.             for ($i = 0; $i < $this->index + 1; $i++)
  513.                 array_shift($parts);
  514.         }
  515.  
  516.         for ($i = 0; $i < count($phrase_parts); $i++) {
  517.             // If quoted string:
  518.             if (substr($phrase_parts[$i], 0, 1) == '"') {
  519.                 if (!$this->_validateQuotedString($phrase_parts[$i]))
  520.                     return false;
  521.                 continue;
  522.             }
  523.  
  524.             // Otherwise it's an atom:
  525.             if (!$this->_validateAtom($phrase_parts[$i])) return false;
  526.         }
  527.  
  528.         return true;
  529.     }
  530.  
  531.     /**
  532.      * Function to validate an atom which from rfc822 is:
  533.      * atom = 1*<any CHAR except specials, SPACE and CTLs>
  534.      * 
  535.      * If validation ($this->validate) has been turned off, then
  536.      * validateAtom() doesn't actually check anything. This is so that you
  537.      * can split a list of addresses up before encoding personal names
  538.      * (umlauts, etc.), for example.
  539.      * 
  540.      * @access private
  541.      * @param string $atom The string to check.
  542.      * @return boolean Success or failure.
  543.      */
  544.     function _validateAtom($atom)
  545.     {
  546.         if (!$this->validate) {
  547.             // Validation has been turned off; assume the atom is okay.
  548.             return true;
  549.         }
  550.  
  551.         // Check for any char from ASCII 0 - ASCII 127
  552.         if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {
  553.             return false;
  554.         }
  555.  
  556.         // Check for specials:
  557.         if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
  558.             return false;
  559.         }
  560.  
  561.         // Check for control characters (ASCII 0-31):
  562.         if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
  563.             return false;
  564.         }
  565.  
  566.         return true;
  567.     }
  568.  
  569.     /**
  570.      * Function to validate quoted string, which is:
  571.      * quoted-string = <"> *(qtext/quoted-pair) <">
  572.      * 
  573.      * @access private
  574.      * @param string $qstring The string to check
  575.      * @return boolean Success or failure.
  576.      */
  577.     function _validateQuotedString($qstring)
  578.     {
  579.         // Leading and trailing "
  580.         $qstring = substr($qstring, 1, -1);
  581.  
  582.         // Perform check.
  583.         return !(preg_match('/(.)[\x0D\\\\"]/', $qstring, $matches) && $matches[1] != '\\');
  584.     }
  585.  
  586.     /**
  587.      * Function to validate a mailbox, which is:
  588.      * mailbox =   addr-spec         ; simple address
  589.      *           / phrase route-addr ; name and route-addr
  590.      * 
  591.      * @access public
  592.      * @param string &$mailbox The string to check.
  593.      * @return boolean Success or failure.
  594.      */
  595.     function validateMailbox(&$mailbox)
  596.     {
  597.         // A couple of defaults.
  598.         $phrase  = '';
  599.         $comment = '';
  600.         $comments = array();
  601.  
  602.         // Catch any RFC822 comments and store them separately
  603.         $_mailbox = $mailbox;
  604.         while (strlen(trim($_mailbox)) > 0) {
  605.             $parts = explode('(', $_mailbox);
  606.             $before_comment = $this->_splitCheck($parts, '(');
  607.             if ($before_comment != $_mailbox) {
  608.                 // First char should be a (
  609.                 $comment    = substr(str_replace($before_comment, '', $_mailbox), 1);
  610.                 $parts      = explode(')', $comment);
  611.                 $comment    = $this->_splitCheck($parts, ')');
  612.                 $comments[] = $comment;
  613.  
  614.                 // +1 is for the trailing )
  615.                 $_mailbox   = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1);
  616.             } else {
  617.                 break;
  618.             }
  619.         }
  620.  
  621.         foreach ($comments as $comment) {
  622.             $mailbox = str_replace("($comment)", '', $mailbox);
  623.         }
  624.  
  625.         $mailbox = trim($mailbox);
  626.  
  627.         // Check for name + route-addr
  628.         if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') {
  629.             $parts  = explode('<', $mailbox);
  630.             $name   = $this->_splitCheck($parts, '<');
  631.  
  632.             $phrase     = trim($name);
  633.             $route_addr = trim(substr($mailbox, strlen($name.'<'), -1));
  634.  
  635.             if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false)
  636.                 return false;
  637.  
  638.         // Only got addr-spec
  639.         } else {
  640.             // First snip angle brackets if present.
  641.             if (substr($mailbox,0,1) == '<' && substr($mailbox,-1) == '>')
  642.                 $addr_spec = substr($mailbox,1,-1);
  643.             else
  644.                 $addr_spec = $mailbox;
  645.  
  646.             if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false)
  647.                 return false;
  648.         }
  649.  
  650.         // Construct the object that will be returned.
  651.         $mbox = new stdClass();
  652.  
  653.         // Add the phrase (even if empty) and comments
  654.         $mbox->personal = $phrase;
  655.         $mbox->comment  = isset($comments) ? $comments : array();
  656.  
  657.         if (isset($route_addr)) {
  658.             $mbox->mailbox = $route_addr['local_part'];
  659.             $mbox->host    = $route_addr['domain'];
  660.             $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : '';
  661.         } else {
  662.             $mbox->mailbox = $addr_spec['local_part'];
  663.             $mbox->host    = $addr_spec['domain'];
  664.         }
  665.  
  666.         $mailbox = $mbox;
  667.         return true;
  668.     }
  669.  
  670.     /**
  671.      * This function validates a route-addr which is:
  672.      * route-addr = "<" [route] addr-spec ">"
  673.      *
  674.      * Angle brackets have already been removed at the point of
  675.      * getting to this function.
  676.      * 
  677.      * @access private
  678.      * @param string $route_addr The string to check.
  679.      * @return mixed False on failure, or an array containing validated address/route information on success.
  680.      */
  681.     function _validateRouteAddr($route_addr)
  682.     {
  683.         // Check for colon.
  684.         if (strpos($route_addr, ':') !== false) {
  685.             $parts = explode(':', $route_addr);
  686.             $route = $this->_splitCheck($parts, ':');
  687.         } else {
  688.             $route = $route_addr;
  689.         }
  690.  
  691.         // If $route is same as $route_addr then the colon was in
  692.         // quotes or brackets or, of course, non existent.
  693.         if ($route === $route_addr){
  694.             unset($route);
  695.             $addr_spec = $route_addr;
  696.             if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
  697.                 return false;
  698.             }
  699.         } else {
  700.             // Validate route part.
  701.             if (($route = $this->_validateRoute($route)) === false) {
  702.                 return false;
  703.             }
  704.  
  705.             $addr_spec = substr($route_addr, strlen($route . ':'));
  706.  
  707.             // Validate addr-spec part.
  708.             if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
  709.                 return false;
  710.             }
  711.         }
  712.  
  713.         if (isset($route)) {
  714.             $return['adl'] = $route;
  715.         } else {
  716.             $return['adl'] = '';
  717.         }
  718.  
  719.         $return = array_merge($return, $addr_spec);
  720.         return $return;
  721.     }
  722.  
  723.     /**
  724.      * Function to validate a route, which is:
  725.      * route = 1#("@" domain) ":"
  726.      * 
  727.      * @access private
  728.      * @param string $route The string to check.
  729.      * @return mixed False on failure, or the validated $route on success.
  730.      */
  731.     function _validateRoute($route)
  732.     {
  733.         // Split on comma.
  734.         $domains = explode(',', trim($route));
  735.  
  736.         for ($i = 0; $i < count($domains); $i++) {
  737.             $domains[$i] = str_replace('@', '', trim($domains[$i]));
  738.             if (!$this->_validateDomain($domains[$i])) return false;
  739.         }
  740.  
  741.         return $route;
  742.     }
  743.  
  744.     /**
  745.      * Function to validate a domain, though this is not quite what
  746.      * you expect of a strict internet domain.
  747.      *
  748.      * domain = sub-domain *("." sub-domain)
  749.      * 
  750.      * @access private
  751.      * @param string $domain The string to check.
  752.      * @return mixed False on failure, or the validated domain on success.
  753.      */
  754.     function _validateDomain($domain)
  755.     {
  756.         // Note the different use of $subdomains and $sub_domains                        
  757.         $subdomains = explode('.', $domain);
  758.  
  759.         while (count($subdomains) > 0) {
  760.             $sub_domains[] = $this->_splitCheck($subdomains, '.');
  761.             for ($i = 0; $i < $this->index + 1; $i++)
  762.                 array_shift($subdomains);
  763.         }
  764.  
  765.         for ($i = 0; $i < count($sub_domains); $i++) {
  766.             if (!$this->_validateSubdomain(trim($sub_domains[$i])))
  767.                 return false;
  768.         }
  769.  
  770.         // Managed to get here, so return input.
  771.         return $domain;
  772.     }
  773.  
  774.     /**
  775.      * Function to validate a subdomain:
  776.      *   subdomain = domain-ref / domain-literal
  777.      * 
  778.      * @access private
  779.      * @param string $subdomain The string to check.
  780.      * @return boolean Success or failure.
  781.      */
  782.     function _validateSubdomain($subdomain)
  783.     {
  784.         if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){
  785.             if (!$this->_validateDliteral($arr[1])) return false;
  786.         } else {
  787.             if (!$this->_validateAtom($subdomain)) return false;
  788.         }
  789.  
  790.         // Got here, so return successful.
  791.         return true;
  792.     }
  793.  
  794.     /**
  795.      * Function to validate a domain literal:
  796.      *   domain-literal =  "[" *(dtext / quoted-pair) "]"
  797.      * 
  798.      * @access private
  799.      * @param string $dliteral The string to check.
  800.      * @return boolean Success or failure.
  801.      */
  802.     function _validateDliteral($dliteral)
  803.     {
  804.         return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\';
  805.     }
  806.  
  807.     /**
  808.      * Function to validate an addr-spec.
  809.      *
  810.      * addr-spec = local-part "@" domain
  811.      * 
  812.      * @access private
  813.      * @param string $addr_spec The string to check.
  814.      * @return mixed False on failure, or the validated addr-spec on success.
  815.      */
  816.     function _validateAddrSpec($addr_spec)
  817.     {
  818.         $addr_spec = trim($addr_spec);
  819.  
  820.         // Split on @ sign if there is one.
  821.         if (strpos($addr_spec, '@') !== false) {
  822.             $parts      = explode('@', $addr_spec);
  823.             $local_part = $this->_splitCheck($parts, '@');
  824.             $domain     = substr($addr_spec, strlen($local_part . '@'));
  825.  
  826.         // No @ sign so assume the default domain.
  827.         } else {
  828.             $local_part = $addr_spec;
  829.             $domain     = $this->default_domain;
  830.         }
  831.  
  832.         if (($local_part = $this->_validateLocalPart($local_part)) === false) return false;
  833.         if (($domain     = $this->_validateDomain($domain)) === false) return false;
  834.         
  835.         // Got here so return successful.
  836.         return array('local_part' => $local_part, 'domain' => $domain);
  837.     }
  838.  
  839.     /**
  840.      * Function to validate the local part of an address:
  841.      *   local-part = word *("." word)
  842.      * 
  843.      * @access private
  844.      * @param string $local_part
  845.      * @return mixed False on failure, or the validated local part on success.
  846.      */
  847.     function _validateLocalPart($local_part)
  848.     {
  849.         $parts = explode('.', $local_part);
  850.  
  851.         // Split the local_part into words.
  852.         while (count($parts) > 0){
  853.             $words[] = $this->_splitCheck($parts, '.');
  854.             for ($i = 0; $i < $this->index + 1; $i++) {
  855.                 array_shift($parts);
  856.             }
  857.         }
  858.  
  859.         // Validate each word.
  860.         for ($i = 0; $i < count($words); $i++) {
  861.             if ($this->_validatePhrase(trim($words[$i])) === false) return false;
  862.         }
  863.  
  864.         // Managed to get here, so return the input.
  865.         return $local_part;
  866.     }
  867.  
  868.     /**
  869.     * Returns an approximate count of how many addresses are
  870.     * in the given string. This is APPROXIMATE as it only splits
  871.     * based on a comma which has no preceding backslash. Could be
  872.     * useful as large amounts of addresses will end up producing
  873.     * *large* structures when used with parseAddressList().
  874.     *
  875.     * @param  string $data Addresses to count
  876.     * @return int          Approximate count
  877.     */
  878.     function approximateCount($data)
  879.     {
  880.         return count(preg_split('/(?<!\\\\),/', $data));
  881.     }
  882.     
  883.     /**
  884.     * This is a email validating function seperate to the rest
  885.     * of the class. It simply validates whether an email is of
  886.     * the common internet form: <user>@<domain>. This can be
  887.     * sufficient for most people. Optional stricter mode can
  888.     * be utilised which restricts mailbox characters allowed
  889.     * to alphanumeric, full stop, hyphen and underscore.
  890.     *
  891.     * @param  string  $data   Address to check
  892.     * @param  boolean $strict Optional stricter mode
  893.     * @return mixed           False if it fails, an indexed array
  894.     *                         username/domain if it matches
  895.     */
  896.     function isValidInetAddress($data, $strict = false)
  897.     {
  898.         $regex = $strict ? '/^([.0-9a-z_-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,4})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,4})$/i';
  899.         if (preg_match($regex, trim($data), $matches)) {
  900.             return array($matches[1], $matches[2]);
  901.         } else {
  902.             return false;
  903.         }
  904.     }
  905. }
  906. ?>
  907.